/*
 * Copyright 2010 WANdisco, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License. 
 */
package com.googlecode.svnj.reports;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;

import javax.xml.stream.XMLStreamException;

import org.apache.jackrabbit.webdav.DavException;
import org.apache.jackrabbit.webdav.DavResource;
import org.apache.jackrabbit.webdav.WebdavRequest;
import org.apache.jackrabbit.webdav.WebdavResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import com.googlecode.svnj.EditorDriver;
import com.googlecode.svnj.RepoContext;
import com.googlecode.svnj.XMLUtil;
import com.googlecode.svnj.dav.IO;

public final class UpdateReport extends ReportRequest {

    private static final Logger log = LoggerFactory.getLogger(UpdateReport.class);
    
    private List<Entry> entries = new ArrayList<Entry>(1);
    private String srcPath, dstPath;
    private String updateTarget = "";
    private long targetRevision = -1;
    private long pegRevision, locationRevision;
    private boolean sendAll, ignoreAncestry, sendCopyFromArgs,
    resourceWalk, textDeltas;
    private Depth depth;
    
    @Override
    public String getReportType() {
        return ReportRequest.TYPE_UPDATE;
    }

    @Override
    public void report(WebdavRequest req, WebdavResponse rsp, DavResource resource)
        throws DavException, IOException, XMLStreamException {
    
        RepoContext repo = IO.get().getRepo(req);
        
        if (targetRevision == -1) {
            // if unspecified, client want update to HEAD
            targetRevision = repo.getYoungestRev();
        }
        
        // make relative to repos path part of our url
        this.srcPath = repo.subPathURL(srcPath);
        this.dstPath = repo.subPathURL(dstPath);
        log.info("running update-report " + this + ", src_path='" + srcPath
                + "' dst_path='" + dstPath + "'");
        long trev = getTargetRevision();
        if (trev < 0) {
            trev = repo.getYoungestRev();
        }
        XMLDeltaEditor xd = null;
        xd = new XMLDeltaEditor(rsp.getOutputStream(), repo);
        EditorDriver ed = new EditorDriver(xd, repo, this);
        try {
            ed.startEdit();
        } finally {
            ed.cleanupSelf();
        }
    }
    /*
    ---- entry sub-schema  ----
    attributes:
     rev
     depth
     linkpath
     start-empty
     lock-token
     */
    public class Entry {
        private long rev;
        private Depth depth;
        private boolean startEmpty;
        private String path, lockToken, linkPath;
        // create simple, typical entry programatically
        private Entry(String path, long rev) {
            this.path = path;
            this.rev = rev;
            this.depth = Depth.INFINITY;
            this.startEmpty = false;
        }
        private Entry(Node e) {
            rev = -1;
            Long l = XMLUtil.getAttributeLong(e, "rev");
            if (l != null)
                rev = l;
            else
                throw new IllegalArgumentException(
                        "Missing XML attribute: rev"
                        );
            depth = Depth.INFINITY;
            String s = XMLUtil.getAttribute(e, "depth");
            if (s != null) depth = Depth.fromString(s);
            startEmpty = XMLUtil.getAttribute(e, "start-empty") != null;
            linkPath = XMLUtil.getAttribute(e, "linkpath");
            lockToken = XMLUtil.getAttribute(e, "lock-token");
            path = e.getTextContent();
        }
        public long getRev() { return rev; }
        public Depth getDepth() { return depth; }
        public boolean isStartEmpty() { return startEmpty; }
        public String getPath() { return path; }
        public String getLockToken() { return lockToken; }
        public String getLinkPath() { return linkPath; }

    }
    /*package*/UpdateReport() {
        // initialize defaults
        depth = Depth.UNKNOWN;
        this.textDeltas = true;
    }
    public UpdateReport(String srcPath, long srcRev, long targetRev) {
        this();
        this.srcPath = srcPath;
        this.targetRevision = targetRev;
        entries.add(new Entry("", srcRev));
    }
    /*
    <S:update-report send-all="true" xmlns:S="svn:">
     <S:src-path>http://localhost:9090/svn/alphabet/another</S:src-path>
     <S:target-revision>7</S:target-revision>
     <S:update-target>another</S:update-target>
     <S:depth>unknown</S:depth>
     <S:send-copyfrom-args>yes</S:send-copyfrom-args>
     <S:entry rev="8" depth="infinity"></S:entry>
     <S:missing></S:missing>
    </S:update-report>
    ----------
    <S:update-report send-all="true" xmlns:S="svn:">
     <S:src-path>http://localhost:9090/svn/alphabet</S:src-path>
     <S:target-revision>2</S:target-revision>
     <S:depth>unknown</S:depth>
     <S:send-copyfrom-args>yes</S:send-copyfrom-args>
     <S:entry rev="1" depth="infinity"></S:entry>
    </S:update-report>
    ---------------------------
    also:
    update-target (path)
    depth
    recursive (yes|no)
    ignore-ancestry (yes|no)
    send-copyfrom-args
    dst-path
    missing (cdata is path, call svn_repos_delete_path()
    cdata is entry path
     * valid depth values exclude|unknown|empty|files|immediates|infinity
     */
    @Override
    protected void parse(Node root) {
        // see comments in mod_dav_svn/reports/update.c
        boolean sawRecursive = false, sawDepth = false;
        // look at attributes
        if (root.getAttributes() != null) {
            Node att = root.getAttributes().getNamedItem("send-all");
            if (att != null) {
                sendAll = "true".equalsIgnoreCase(att.getNodeValue()); 
            }
        }
        // walk elements
        NodeList children = root.getChildNodes();
        for (int i = 0; i < children.getLength(); i++) {
            Node n = children.item(i);
            String name = n.getLocalName();
            if ("src-path".equals(name)) {
                // might be URI; make relative in report phase
                srcPath = n.getTextContent();
            } else if ("target-revision".equals(name)) {
                targetRevision = Long.parseLong(n.getTextContent());
            } else if ("dst-path".equals(name)) {
                dstPath = n.getTextContent();
            } else if ("update-target".equals(name)) {
                updateTarget = n.getTextContent();
            } else if ("depth".equals(name)) {
                depth = Depth.fromString(n.getTextContent());
                sawDepth = true;
            } else if ("recursive".equals(name)) {
                depth = "no".equals(n.getTextContent()) ?
                    Depth.FILES: Depth.INFINITY;
                sawDepth = true;
            } else if ("ignore-ancestry".equals(name)) {
                ignoreAncestry = !"no".equals(n.getTextContent());
            } else if ("send-copyfrom-args".equals(name)) {
                sendCopyFromArgs = !"no".equals(n.getTextContent());
            } else if ("resource-walk".equals(name)) {
                resourceWalk = !"no".equals(n.getTextContent());
            } else if ("text-deltas".equals(name)) {
                 textDeltas = !"no".equals(n.getTextContent());
            } else if ("entry".equals(name)) {
                Entry e= new Entry(n);
                entries.add(e);
                /* FIXME: these hacks are not right,
                 * should be handled on entry-by-entry basis,
                 * but they keep the tests passing for now...
                 */
                /*if (e.getRev() != -1) this.startRevision = e.getRev();
                if (e.isStartEmpty()) { this.startRevision = 0; }*/
            }
            if (!sawDepth && !sawRecursive && depth == Depth.UNKNOWN) {
                depth = Depth.INFINITY;
            }
        }
        if (srcPath == null) {
            throw new IllegalArgumentException(
                    "The request did not contain the '<src-path>' element,\n"
                    + "which indicates that the client is too old."
                    );
        }
    }

    public List<Entry> getEntries() { return entries; }
    public String getDstPath() { return dstPath; }
    public String getUpdateTarget() { return updateTarget; }
    public long getPegRevision() { return pegRevision; }
    public long getLocationRevision() { return locationRevision; }
    public boolean isSendAll() { return sendAll; }
    public boolean isIgnoreAncestry() { return ignoreAncestry; }
    public boolean isSendCopyFromArgs() { return sendCopyFromArgs; }
    public boolean isResourceWalk() { return resourceWalk; }
    public boolean isTextDeltas() { return textDeltas; }
    public Depth getDepth() { return depth; }
    public String getSrcPath() { return srcPath; }
    public long getTargetRevision() { return targetRevision; }
    //public long getStartRevision() { return startRevision; }
    
    public String toString() {
        return "[REPORT: " + getReportType() + " ath=" + srcPath
        + " targetRev=" + targetRevision + " target='" + updateTarget + "']";
    }
}
